package com.proto {
	/**
	 * ...
	 * @author chenlong
	 */
	public class Parser {
		
		private var tn:Tokenizer;
		/**是否解析proto3**/
		private var proto3:Boolean = false;
		
		public function Parser( source:String ) {
			
			this.tn = new Tokenizer( source );
		}
		
		//public function new(proto:String): Parser{
			//
			//return null;
		//}
		
        public function parse(): MetaProto2{
			
			//var topLevel:MetaProto = new MetaProto();
			var topLevel:MetaProto2 = new MetaProto2();
			topLevel.name = "[ROOT]";
			topLevel.syntax = "proto3";
			topLevel.packages = null;
			topLevel.messages = [];// new Vector.<ProtoMessage>;
			topLevel.enums = [];//new Vector.<ProtoEnum>;
			topLevel.imports = [];//new Vector.<String>;
			topLevel.options = {};
			topLevel.services = [];//new Vector.<ProtoService>;
			topLevel.lines	= {};
			topLevel.comments = {};
			
			var token:String;
            var head:Boolean = true;
            try {
                while ( (token = this.tn.next()) != null  ) { //token = this.tn.next()
                    switch (token) {
                        case 'package':
                            if (!head || topLevel["packages"] !== null)
                                throw new Error("unexpected 'package'");
                            token = this.tn.next();
                            if (!Lang.TYPEREF.test(token))
                                throw new Error("illegal package name: " + token);
                            this.tn.skip(";");
                            topLevel["packages"] = token;
							topLevel.lines["package"] = this.tn.line;
                            break;
                        case 'import':
                            if (!head)
                                throw new Error("unexpected 'import'");
                            token = this.tn.peek();
                            if (token === "public") // ignored
                                this.tn.next();
                            token = this._readString();
                            this.tn.skip(";");
                            topLevel["imports"].push(token);
                            break;
                        case 'syntax':
                            if (!head)
                                throw new Error("unexpected 'syntax'");
                            this.tn.skip("=");
                            if ((topLevel["syntax"] = this._readString()) === "proto3")
                                this.proto3 = true;
                            this.tn.skip(";");
							topLevel.lines["syntax"] = this.tn.line;
                            break;
                        case 'message':
                            this._parseMessage(topLevel, null);
                            head = false;
                            break;
                        case 'enum':
                            this._parseEnum(topLevel);
                            head = false;
                            break;
                        case 'option':
                            this._parseOption(topLevel);
                            break;
                        case 'service':
                            this._parseService(topLevel);
                            break;
                        case 'extend':
                            this._parseExtend(topLevel);
                            break;
                        default:
                            throw new Error("unexpected '" + token + "'");
                    }
					//token = this.tn.next();
                }
            } catch (e:Error) {
                e.message = "Parse error at line "+this.tn.line+": " + e.message;
                throw e;
            }
			topLevel.comments = this.tn.comments;
            delete topLevel["name"];			
            return topLevel;
		}
		
		/**
		 * Reads a string.
		 * @return
		 */
		private  function _readString():String{
			
			var value:String = "";
            var token:String = "";
            var delim:String = "";
            do {
                delim = this.tn.next();
                if (delim !== "'" && delim !== '"')
                    throw Error("illegal string delimiter: "+delim);
                value += this.tn.next();
                this.tn.skip(delim);
                token = this.tn.peek();
            } while (token === '"' || token === '"'); // multi line?
            return value;
		}
		
		/**
		 * Reads a value.
		 * @return
		 */
		private function _readValue( mayBeTypeRef:* ):*{
			
			var token:String = this.tn.peek();
            if (token === '"' || token === "'")
                return this._readString();
            this.tn.next();
            if (Lang.NUMBER.test(token))
                return mkNumber(token);
            if (Lang.BOOL.test(token))
                return (token.toLowerCase() === 'true');
            if (mayBeTypeRef && Lang.TYPEREF.test(token))
                return token;
            throw Error("illegal value: "+token);
		}
		
		/**
		 * Parses a namespace option.
		 * @param	parent {!Object} Parent definition
		 * @param	isList
		 * @return
		 */
		private function _parseOption( parent:* , isList:Boolean = false ):*{
			
			var token:String = this.tn.next();
            var custom:Boolean = false;
            if (token === '(') {
                custom = true;
                token = this.tn.next();
            }
            if (!Lang.TYPEREF.test(token))
                // we can allow options of the form google.protobuf.* since they will just get ignored anyways
                // if (!/google\.protobuf\./.test(token)) // FIXME: Why should that not be a valid typeref?
                    throw Error("illegal option name: "+token);
            var name:String = token;
            if (custom) { // (my_method_option).foo, (my_method_option), some_method_option, (foo.my_option).bar
                this.tn.skip(')');
                name = '('+name+')';
                token = this.tn.peek();
                if (Lang.FQTYPEREF.test(token)) {
                    name += token;
                    this.tn.next();
                }
            }
            this.tn.skip('=');
            this._parseOptionValue(parent, name);
            if (!isList)
                this.tn.skip(";");
		}
		
		/**
		 * Parses an option value.
		 * @param	parent {!Object}
		 * @param	name
		 */
		private function _parseOptionValue( parent:* , name:String ):void{
			
			var token:String = this.tn.peek();
            if (token !== '{') { // Plain value
                setOption(parent["options"], name, this._readValue(true));
            } else { // Aggregate options
                this.tn.skip("{");
                while ((token = this.tn.next()) !== '}') {
                    if (!Lang.NAME.test(token))
                        throw Error("illegal option name: " + name + "." + token);
                    if (this.tn.omit(":"))
                        setOption(parent["options"], name + "." + token, this._readValue(true));
                    else
                        this._parseOptionValue(parent, name + "." + token);
                }
            }
		}
		
		/**
		 * Parses a service definition.
		 * @param	parent  {!Object}Parent definition
		 */
		private function _parseService( parent:* ):void{
			
			var token:String = this.tn.next();
            if (!Lang.NAME.test(token))
                throw Error("illegal service name at line "+this.tn.line+": "+token);
            var name:String = token;
            var svc:Object = {
                "name": name,
                "rpc": {},
                "options": {}
            };
            this.tn.skip("{");
            while ((token = this.tn.next()) !== '}') {
                if (token === "option")
                    this._parseOption(svc);
                else if (token === 'rpc')
                    this._parseServiceRPC(svc);
                else
                    throw new Error("illegal service token: "+token);
            }
            this.tn.omit(";");
            parent["services"].push(svc);
		}
		
		private function _parseServiceRPC( svc:Object ):void{
			
			var type:String = "rpc";
            var token:String = this.tn.next();
            if (!Lang.NAME.test(token))
                throw new Error("illegal rpc service method name: "+token);
            var name:String = token;
            var method:Object = {
                "request": null,
                "response": null,
                "request_stream": false,
                "response_stream": false,
                "options": {}
            };
            this.tn.skip("(");
            token = this.tn.next();
            if (token.toLowerCase() === "stream") {
              method["request_stream"] = true;
              token = this.tn.next();
            }
            if (!Lang.TYPEREF.test(token))
                throw new Error("illegal rpc service request type: "+token);
            method["request"] = token;
            this.tn.skip(")");
            token = this.tn.next();
            if (token.toLowerCase() !== "returns")
                throw new Error("illegal rpc service request type delimiter: "+token);
            this.tn.skip("(");
            token = this.tn.next();
            if (token.toLowerCase() === "stream") {
              method["response_stream"] = true;
              token = this.tn.next();
            }
            method["response"] = token;
            this.tn.skip(")");
            token = this.tn.peek();
            if (token === '{') {
                this.tn.next();
                while ((token = this.tn.next()) !== '}') {
                    if (token === 'option')
                        this._parseOption(method);
                    else
                        throw new Error("illegal rpc service token: " + token);
                }
                this.tn.omit(";");
            } else
                this.tn.skip(";");
            if ( svc[type] === undefined )//typeof svc[type] === 'undefined'
                svc[type] = {};
            svc[type][name] = method;
		}
		
		/**
         * Parses a message definition.
         * @param {!Object} parent Parent definition
         * @param {!Object=} fld Field definition if this is a group
         * @return {!Object}
         * @private
         */
		private function _parseMessage( parent:* , fld:* = null):Object{
			
			var isGroup:Boolean = !!fld;
            var token:String = this.tn.next();
            var msg:Object = {
                "name": "",
                "fields": [],
                "enums": [],
                "messages": [],
                "options": {},
                "services": [],
                "oneofs": {},
				"comment":"",
				"line":0
                // "extensions": undefined
            };
            if (!Lang.NAME.test(token))
                throw Error("illegal "+(isGroup ? "group" : "message")+" name: "+token);
            msg["name"] = token;
			msg["line"] = this.tn.line;
            if (isGroup) {
                this.tn.skip("=");
                fld["id"] = mkId(this.tn.next());
                msg["isGroup"] = true;
            }
            token = this.tn.peek();
            if (token === '[' && fld)
                this._parseFieldOptions(fld);
            this.tn.skip("{");
            while ((token = this.tn.next()) !== '}') {
                if (Lang.RULE.test(token)){					
                    this._parseMessageField(msg, token);
				}
                else if (token === "oneof")
                    this._parseMessageOneOf(msg);
                else if (token === "enum")
                    this._parseEnum(msg);
                else if (token === "message")
                    this._parseMessage(msg);
                else if (token === "option")
                    this._parseOption(msg);
                else if (token === "service")
                    this._parseService(msg);
                else if (token === "extensions")
                    msg["extensions"] = this._parseExtensionRanges();
                else if (token === "reserved")
                    this._parseIgnored(); // TODO
                else if (token === "extend")
                    this._parseExtend(msg);
                else if (Lang.TYPEREF.test(token)) {
                    if (!this.proto3)
                        throw Error("illegal field rule: "+token);
                    this._parseMessageField(msg, "optional", token);
                } else
                    throw Error("illegal message token: "+token);
            }
            this.tn.omit(";");
            parent["messages"].push(msg);
            return msg;
		}
		
		private function _parseIgnored():void{
			
			while (this.tn.peek() !== ';')
                this.tn.next();
            this.tn.skip(";");
		}
		
		/**
         * Parses a message field.
         * @param {!Object} msg Message definition
         * @param {string} rule Field rule
         * @param {string=} type Field type if already known (never known for maps)
         * @return {!Object} Field descriptor
         * @private
         */
		private function _parseMessageField(msg:*, rule:String, type:String= "" ):*{
			
			if (!Lang.RULE.test(rule))
                throw new Error("illegal message field rule: "+rule);
            var fld:Object = {
                "rule": rule,
                "type": "",
                "name": "",
                "options": {},
                "id": 0,
				"comment":"",
				"line":0
            };
            var token:String;
            if (rule === "map") {

                if (type)
                    throw new Error("illegal type: " + type);
                this.tn.skip('<');
                token = this.tn.next();
                if (!Lang.TYPE.test(token) && !Lang.TYPEREF.test(token))
                    throw new Error("illegal message field type: " + token);
                fld["keytype"] = token;
                this.tn.skip(',');
                token = this.tn.next();
                if (!Lang.TYPE.test(token) && !Lang.TYPEREF.test(token))
                    throw new Error("illegal message field: " + token);
                fld["type"] = token;
                this.tn.skip('>');
                token = this.tn.next();
                if (!Lang.NAME.test(token))
                    throw new Error("illegal message field name: " + token);
                fld["name"] = token;
                this.tn.skip("=");
                fld["id"] = mkId(this.tn.next());
                token = this.tn.peek();
                if (token === '[')
                    this._parseFieldOptions(fld);
                this.tn.skip(";");

            } else {

                type = type ? type : this.tn.next(); //typeof type !== 'undefined'

                if (type === "group") {

                    // "A [legacy] group simply combines a nested message type and a field into a single declaration. In your
                    // code, you can treat this message just as if it had a Result type field called result (the latter name is
                    // converted to lower-case so that it does not conflict with the former)."
                    var grp:Object = this._parseMessage(msg, fld);
                    if (!/^[A-Z]/.test(grp["name"]))
                        throw Error('illegal group name: '+grp["name"]);
                    fld["type"] = grp["name"];
                    fld["name"] = grp["name"].toLowerCase();
                    this.tn.omit(";");

                } else {

                    if (!Lang.TYPE.test(type) && !Lang.TYPEREF.test(type))
                        throw new Error("illegal message field type: " + type);
                    fld["type"] = type;
                    token = this.tn.next();
                    if (!Lang.NAME.test(token))
                        throw new Error("illegal message field name: " + token);
                    fld["name"] = token;
                    this.tn.skip("=");
                    fld["id"] = mkId(this.tn.next());
                    token = this.tn.peek();
                    if (token === "[")
                        this._parseFieldOptions(fld);
                    this.tn.skip(";");

                }
            }
			fld["line"] = this.tn.line;
            msg["fields"].push(fld);
            return fld;
		}
		
		private function _parseMessageOneOf(msg:Object):void{
			
			var token:String = this.tn.next();
            if (!Lang.NAME.test(token))
                throw Error("illegal oneof name: "+token);
            var name:String = token;
            var fld:Object;
            var fields:Array = [];
            this.tn.skip("{");
            while ((token = this.tn.next()) !== "}") {
                fld = this._parseMessageField(msg, "optional", token);
                fld["oneof"] = name;
                fields.push(fld["id"]);
            }
            this.tn.omit(";");
            msg["oneofs"][name] = fields;
		}
		
		private function _parseFieldOptions( fld:Object ):void{
			
			this.tn.skip("[");
            var token:String;
            var first:Boolean = true;
            while ((token = this.tn.peek()) !== ']') {
                if (!first)
                    this.tn.skip(",");
                this._parseOption(fld, true);
                first = false;
            }
            this.tn.next();
		}
		
		private function _parseEnum(msg:*):void{
			
			 var enm:Object = {
                "name": "",
                "values": [],
                "options": {}
            };
            var token:String = this.tn.next();
            if (!Lang.NAME.test(token))
                throw new Error("illegal name: "+token);
            enm["name"] = token;
            this.tn.skip("{");
            while ((token = this.tn.next()) !== '}') {
                if (token === "option")
                    this._parseOption(enm);
                else {
                    if (!Lang.NAME.test(token))
                        throw new Error("illegal name: "+token);
                    this.tn.skip("=");
                    var val:Object = {
                        "name": token,
                        "id": mkId(this.tn.next(), true)
                    };
                    token = this.tn.peek();
                    if (token === "[")
                        this._parseFieldOptions({ "options": {} });
                    this.tn.skip(";");
                    enm["values"].push(val);
                }
            }
            this.tn.omit(";");
            msg["enums"].push(enm);
		}
		
		private function _parseExtensionRanges():Array{
			
			var ranges:Array = [];
            var token:String;
            var range:Array;
            var value:uint;
            do {
                range = [];
                while (true) {
                    token = this.tn.next();
                    switch (token) {
                        case "min":
                            value = ProtoBuf.ID_MIN;
                            break;
                        case "max":
                            value = ProtoBuf.ID_MAX;
                            break;
                        default:
                            value = mkNumber(token);
                            break;
                    }
                    range.push(value);
                    if (range.length === 2)
                        break;
                    if (this.tn.peek() !== "to") {
                        range.push(value);
                        break;
                    }
                    this.tn.next();
                }
                ranges.push(range);
            } while (this.tn.omit(","));
            this.tn.skip(";");
            return ranges;
		}
		
		private function _parseExtend( parent:* ):Object{
			
			var token:String = this.tn.next();
            if (!Lang.TYPEREF.test(token))
                throw new Error("illegal extend reference: "+token);
            var ext:Object = {
                "ref": token,
                "fields": []
            };
            this.tn.skip("{");
            while ((token = this.tn.next()) !== '}') {
                if (Lang.RULE.test(token))
                    this._parseMessageField(ext, token);
                else if (Lang.TYPEREF.test(token)) {
                    if (!this.proto3)
                        throw new Error("illegal field rule: "+token);
                    this._parseMessageField(ext, "optional", token);
                } else
                    throw new Error("illegal extend token: "+token);
            }
            this.tn.omit(";");
            parent["messages"].push(ext);
            return ext;
		}
		
        public function get toString():String{
			
			return "Parser at line "+this.tn.line;
		}
		
		/**
		 * 解析proto文件
		 * @param	proto
		 * @return
		 */
		public static function parse( proto:String ):MetaProto2{
			
			return new Parser( proto ).parse();
		}
		
		
		/**
		 * Converts a numerical string to an id.
		 * @param	value
		 * @param	mayBeNegative
		 * @return
		 */
		public static function mkId( value:String , mayBeNegative:* = null ):int{
			
			var id:int = -1;
            var sign:int = 1;
            if (value.charAt(0) == '-') {
                sign = -1;
                value = value.substring(1);
            }
            if (Lang.NUMBER_DEC.test(value))
                id = parseInt(value);
            else if (Lang.NUMBER_HEX.test(value))
                id = parseInt(value.substring(2), 16);
            else if (Lang.NUMBER_OCT.test(value))
                id = parseInt(value.substring(1), 8);
            else
                throw new Error("illegal id value: " + (sign < 0 ? '-' : '') + value);
            id = (sign*id)|0; // Force to 32bit
            if (!mayBeNegative && id < 0)
                throw new Error("illegal id value: " + (sign < 0 ? '-' : '') + value);
            return id;
		}
		
		/**
		 * 
		 * @param	val
		 */
		public static function mkNumber(val:String):Number{
            var sign:int = 1;
            if (val.charAt(0) == '-') {
                sign = -1;
                val = val.substring(1);
            }
            if (Lang.NUMBER_DEC.test(val))
                return sign * parseInt(val, 10);
            else if (Lang.NUMBER_HEX.test(val))
                return sign * parseInt(val.substring(2), 16);
            else if (Lang.NUMBER_OCT.test(val))
                return sign * parseInt(val.substring(1), 8);
            else if (val === 'inf')
                return sign * Infinity;
            else if (val === 'nan')
                return NaN;
            else if (Lang.NUMBER_FLT.test(val))
                return sign * parseFloat(val);
            throw Error("illegal number value: " + (sign < 0 ? '-' : '') + val);
			return 0;
        } 
		
		 /**
         * Sets an option on the specified options object.
         * @param {!Object.<string,*>} options
         * @param {string} name
         * @param {string|number|boolean} value
         */
        public static function setOption(options:*, name:String, value:*):void {
            if (options[name] == undefined)
                options[name] = value;
            else {
                if (!(options[name] is Array || options[name] is Vector))
                    options[name] = [ options[name] ];
                options[name].push(value);
            }
        }
	}

}